
/**
* Highstock plugin for displaying current price indicator.
*
* Author: Roland Banguiran
* Email: banguiran@gmail.com
*
*/

// JSLint options:
/*global Highcharts, document */

(function(H) {
    "use strict";
    var merge = H.merge;
    var highChartFGColorsMap = {"#04FD03": 'black',"white":'black', "beige":'black', "silver":'black', "#FFFF00":"black", "cyan":"black"};


    H.wrap(H.Chart.prototype, "init", function(proceed) {

        // Run the original proceed method
        proceed.apply(this, Array.prototype.slice.call(arguments, 1));

        renderCurrentPriceIndicator(this);
    });

    H.wrap(H.Chart.prototype, "redraw", function(proceed) {

        // Run the original proceed method
        proceed.apply(this, Array.prototype.slice.call(arguments, 1));

        renderCurrentPriceIndicator(this);
    });

    H.wrap(H.Chart.prototype, "setAxisType", function(proceed) {
      var localArgs = Array.prototype.slice.call(arguments, 1);
      proceed.apply(this, localArgs );

      var indicatorCache = this.indicatorCache;
      if ( isValued( indicatorCache ) ) {
        indicatorCache.reset();
      }

    });
    function IndicatorCache(){

    };

  IndicatorCache.prototype.reset= function() {
    var seriesNames = Object.keys(this);
    for ( var index = 0; index < seriesNames.length; index++ ) {
      var seriesName = seriesNames[ index ];
      deleteLine( this[ seriesName ] );
      this[ seriesName ] = undefined;
    }
  };

    IndicatorCache.prototype.clearSeries= function( seriesList ) {
        var localCache = this;
        if ( ! seriesList ) return;
        seriesList.forEach(function( series ) {
            var seriesIndicator = localCache[ series.name ];
            if ( seriesIndicator != null ) {
                deleteLine( seriesIndicator );
                delete localCache[ series.name ];
            }
            
        });
    };

    function renderCurrentPriceIndicator(chart) {
        if ( chart.yAxis.length == 0 || chart.series.length == 0 ) {
            return;
        }

        var enabledOnChart = chart.options.performance.currentPriceIndicator;
        if ( enabledOnChart == false ) {
            if ( Object.keys( chart.indicatorCache ).length > 0  ) {
                deleteAllLines( chart.indicatorCache );
                chart.indicatorCache = new IndicatorCache();
            }
            return;
        }
       //colorFloatingLegend( chart );

        var allSeries = chart.getRenderedSeries();
        for ( var index = 0; index < allSeries.length; index++ ) {
            var series = allSeries[ index ];
            var disabled = ( series.options.hasOwnProperty("showCurrentPriceIndicator") && !series.options.showCurrentPriceIndicator );

            if ( !series || !series.visible || disabled || series.options.linkedTo ) continue;
            renderCurrentPriceIndicatorSeries(chart, series);
        }

        cleanupOldLines( chart );
    }

    function destroyElement( element ) {
        if ( isDefined( element ) && element != null ) {
            element.destroy();
        }
    }

    function deleteLine( currentPriceIndicator ) {
        destroyElement( currentPriceIndicator.group );
        destroyElement(currentPriceIndicator.label );
        destroyElement( currentPriceIndicator.box );
        destroyElement( currentPriceIndicator.line );
    }

    function cleanupOldLines( chart ) {
        var indicatorCache = chart.indicatorCache;
        if ( ! indicatorCache ) return;

        var keys = Object.keys(indicatorCache);
        if ( keys == null ) return;
        keys.forEach( function( key ) {
          var currentPriceIndicator = indicatorCache[ key ];
          if ( currentPriceIndicator ) {
            var series = chart.getSeriesByName( key );
            if ( ! series || !series.visible) {
              deleteLine ( currentPriceIndicator );
              delete indicatorCache[ key ];
            }
          }

        });
    }

    function getFirstAxisTick( axis ) {
        if ( !axis.tickPositions || axis.tickPositions.length == 0 ) return null;
        var tickPosition = axis.tickPositions[0];
        return axis.ticks[ tickPosition ];
    }

    function getLastAxisTick( axis ) {
        if ( !axis.tickPositions || axis.tickPositions.length == 0 ) return null;
        var tickPosition = axis.tickPositions[axis.tickPositions.length -1];
        return axis.ticks[ tickPosition ];
    }

    function formatDouble( value, axis ) {
        var priceValue = value;
        if ( Array.isArray ( value ) ) {
            priceValue = value[ value.length - 1 ];
        }
        return priceValue.toFixed( 3 );
    }

    function getFGColor(bgcolor) {
        var c = highChartFGColorsMap[bgcolor];
        if (c == null) {
            return "#000000";
        } else {
            return colorsToHex[c];
        }
    }


    function colorFloatingLegend( chart ) {
        var legend = chart.legend;
        var updated = false;
        if ( !isDefined( legend ) ) return;
        legend.allItems.forEach( function( item ) {
            if ( item.visible == false ) return;
            var index = item.index;
            if ( !isValued( index )) return;
            var series = chart.series[ index ];
            if ( !isValued( series ) ) {
                console.log( "Cannot find series index", index );
                return;
            }

            if ( item.legendItem && item.legendItem.element.children && item.legendItem.element.children.length > 1 ) {
                var priceItem = item.legendItem.element.children[ 1 ];
                var style = 'fill:'+getFGColor( series.color );
                if ( style != priceItem.getAttribute( "style" ) ) {
                    priceItem.setAttribute( "style", style );
                    priceItem.isDirty = true;
                    updated = true;

                    var box = item.legendItem.renderer.rect( priceItem.offsetLeft, priceItem.offsetTop, priceItem.offsetWidth, priceItem.offsetHeight );
                    box.attr("fill", series.color );
                    box.add();

                    //item.legendItem.element.insertBefore( box, priceItem );
                }
            }
        });
        if ( updated ) {
            chart.isDirtyLegend = true;
            legend.render();
        }
    }

    function getLargestTickLength( axis ) {
      if ( !isDefined( axis ) || !isDefined( axis.tickPositions ) ) return 0;
      var largestLength = 0;
      axis.tickPositions.forEach( function( tickPosition ) {
        var tick = axis.ticks[ tickPosition ];
        if ( tick.labelLength > largestLength ) largestLength = tick.labelLength;
      });

      return largestLength;
    }

    function renderCurrentPriceIndicatorSeries(chart, priceSeries) {
        var indicatorCache = chart.indicatorCache;
        if ( !indicatorCache ) {
            indicatorCache = new IndicatorCache();
            chart.indicatorCache = indicatorCache;
        }
        var priceYAxis = priceSeries.yAxis;
        var priceData = priceSeries.yData;
        if ( priceData.length == 0 ) return;
        var currentPrice = priceData[priceData.length -1 ];
        if ( currentPrice == null ) {
            //this is a placeholder point..
            if ( priceSeries.linkedSeries.length == 0 ) {
                return;
            }
            //we might have data in the historical part of the chart if we have no live data
            var linkedYData = priceSeries.linkedSeries[ 0 ].yData;
            if ( !linkedYData || linkedYData.length == 0 ) {
                return;
            }                
            currentPrice = linkedYData[ linkedYData.length - 1 ];
            if ( Array.isArray( currentPrice ) ) {
                //we will put close price if OHLC
                currentPrice = currentPrice[ currentPrice.length - 1 ];
            }
        }

        var currentPriceFormatted = formatDouble( currentPrice, priceYAxis );
        if ( Array.isArray( currentPrice ) ) {
            currentPrice = currentPrice[ currentPrice.length - 1];
        }
        if ( isNaN( currentPrice ) ) return;



        var seriesName = priceSeries.options.name;


        var  extremes = priceYAxis.getExtremes(),
            min = extremes.min,
            max = extremes.max,

            defaultOptions = {
                backgroundColor: "#000000",
                borderColor: "#000000",
                lineColor: "#000000",
                lineDashStyle: "line",
                lineOpacity: 0.5,
                enabled: true,
                style: {
                    color: "#ffffff",
                    fontSize: "1.1em",
                    fill:"#000000"
                },
                x: 0,
                y: 0,
                zIndex: 7
            },

            chartWidth = chart.chartWidth,
            chartHeight = chart.chartHeight,
            marginRight = chart.optionsMarginRight || 0,
            marginLeft = chart.optionsMarginLeft || 0,
            renderer = chart.renderer;

            var currentPriceIndicator = indicatorCache[seriesName] || {};
            var oppositeSwitched = ( currentPriceIndicator.opposite ) ? currentPriceIndicator.opposite : false;

            if ( oppositeSwitched || ( currentPriceIndicator.yAxisId && currentPriceIndicator.yAxisId != priceYAxis.options.id ) )  {
                //delete the old one..we are moving axis
                deleteLine( currentPriceIndicator );
                delete indicatorCache[ seriesName ]; //start over
                currentPriceIndicator = {};
            }
            var options = currentPriceIndicator.options || {};
            options.lineColor = priceSeries.color;


            //second is for if the series is removed and readded..
            var isRendered = Object.keys(currentPriceIndicator).length > 0 && currentPriceIndicator.group.parentGroup.hasOwnProperty( "element"),

            group = currentPriceIndicator.group,
            label = currentPriceIndicator.label,
            box = currentPriceIndicator.box,
            line = currentPriceIndicator.line,

            width,
            height,
            x,
            y,

            lineFrom;

        if ( priceSeries.options.color ) options.lineColor = priceSeries.options.color;


        options = merge(true, defaultOptions, options);

        width = priceYAxis.opposite ? (marginRight ? marginRight : 40) : (marginLeft ? marginLeft : 40);
        //x = priceYAxis.opposite ? chartWidth - width : chart.plotBox.x + chart.plotBorderWidth;
        var firstTick = getLastAxisTick( priceYAxis );
        if ( firstTick == null ) return;

        var labelBoxWidth = getLargestTickLength(priceYAxis ) + ( priceYAxis.opposite ? 1 : -1) * + priceYAxis.options.labels.x - 6;
        //their parent anchor points are different..
        if ( priceYAxis.opposite ) {
            x = priceYAxis.axisGroup.getBBox().x + priceYAxis.options.labels.x;
            //offset of 4 to make the bigger fonts take a little space from the padding/tick
            x = x - 16; //take up more space on the tick mark
        } else {
            if ( firstTick == null || firstTick.label == null ) return;
            //alignment of the labels, need to shift the X by the width
            x = firstTick.label.xy.x;
            x = x - labelBoxWidth;
            x = x + 9;
          //take some tick space
        }

        y = priceYAxis.toPixels(currentPrice, false);

        lineFrom = chart.plotLeft;
        var lineTo = lineFrom + chart.plotWidth;
        // offset
        x += options.x;
        y += options.y;
        //defer to the next render
        if ( isNaN( y ) ) return;

        if (options.enabled) {

            // render or animate
            if (!isRendered) {
                // group
                group = renderer.g()
                    .attr({
                    zIndex: options.zIndex
                }).add( priceYAxis.labelGroup);
                // Dynamic color contrast -- http://ux.stackexchange.com/questions/8297/choosing-high-contrast-text-color-in-relation-to-background-color-dynamically
                var fc = getFGColor(priceSeries.color);
                //var c1 = hexToRgb(priceSeries.color);
                //if (c1 != null) {					
                  //var y1 = 0.2126 * Math.pow((c1.r/255), 2.2)  +  0.7151 * Math.pow((c1.g/255), 2.2)  +  0.0721 * Math.pow((c1.b/255),2.2);
                  //if (y1 > 0.18) {
                  //	fc = "#000000"
                  //}
                //}

                // label
                label = renderer.text( currentPriceFormatted, x+4, y)
                    .attr({
                        zIndex: 5
                })
                    .css({
                    //color: options.style.color,
                    	color: fc,
                    //fontSize: options.style.fontSize
                    //fontSize: getDefaultFontSize()
                    fontSize: "1.1em"
                });

                label.add(group);

                //x -= label.getBBox().width;

                height = label.getBBox().height;
                labelBoxWidth = Math.max( labelBoxWidth, label.getBBox().width );
                // box path(["M", lineFrom, y, "L", lineTo, y])
                // box = renderer.rect(x-1, y - (height / 2)-2, labelBoxWidth+1, height+2)
                box = renderer.path(["M", x-4, y ,"L",x+2,y + (height / 2),"L",x+2+42,y + (height / 2),"L",x+2+42,y - (height / 2),"L",x+2,y - (height / 2),"L",x-4, y,"Z"]).attr({
                    fill: options.backgroundColor,
                    stroke: options.borderColor,
                    rx:"4",
                    ry:"2",
                    zIndex: 3,
                        "stroke-width": 1
                })
                	.css({
                    //color: options.style.color,
                		color: priceSeries.color,
                    fontSize: options.style.fontSize
                	})
                .add(group);

                // box
                line = renderer.path(["M", lineFrom, y, "L", lineTo, y])
                    .attr({
                    stroke: options.lineColor,
                    "stroke-dasharray": dashStyleToArray(options.lineDashStyle, 1),
                    "stroke-width": 1,
                    opacity: options.lineOpacity,
                    zIndex: 4,
                })
                .add(group);

                // adjust
                label.animate({
                    y: y + (height / 4)
                }, 0);
            } else {
                currentPriceIndicator.label.animate({
                    text: currentPriceFormatted,
                    y: y
                }, 0);

                height = currentPriceIndicator.label.getBBox().height;

                currentPriceIndicator.box.animate({
                    width: currentPriceIndicator.label.getBBox().width,
                    y: y - (height / 2)
                }, 0);

                currentPriceIndicator.line.animate({
                    d: ["M", lineFrom, y, "L", lineTo, y]
                }, 0);

                // adjust
                currentPriceIndicator.label.animate({
                    y: y + (height / 4)
                }, 0);
            }

            if (currentPrice >= min && currentPrice <= max) {
                group.show();
            } else {
                group.hide();
            }


            // register to price y-axis object
            indicatorCache[seriesName] = {
                group: group,
                label: label,
                box: box,
                line: line,
                opposite: priceYAxis.opposite,
                yAxisId: priceYAxis.options.id
            }
        }
    };

    /**
     * Convert dash style name to array to be used a the value
     * for SVG element"s "stroke-dasharray" attribute
     * @param {String} dashStyle	Possible values: "Solid", "Shortdot", "Shortdash", etc
     * @param {Integer} width	SVG element"s "stroke-width"
     * @param {Array} value
     */
    function dashStyleToArray(dashStyle, width) {
        var value;
        var i;

        dashStyle = dashStyle.toLowerCase();
        width = (typeof width !== "undefined" && width !== 0) ? width : 1;

        if (dashStyle === "solid") {
            value = "none";
        } else if (dashStyle) {
            value = dashStyle
                .replace("shortdashdotdot", "3,1,1,1,1,1,")
                .replace("shortdashdot", "3,1,1,1")
                .replace("shortdot", "1,1,")
                .replace("shortdash", "3,1,")
                .replace("longdash", "8,3,")
                .replace(/dot/g, "1,3,")
                .replace("dash", "4,3,")
                .replace(/,$/, "")
                .split(","); // ending comma

            i = value.length;
            while (i--) {
                value[i] = parseInt(value[i]) * width;
            }
            value = value.join(",");
        }

        return value;
    };

	
    function hexToRgb(hex) {
        // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
        var result;
        if (stringStartsWith(hex, "#")) {
          var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
          hex = hex.replace(shorthandRegex, function(m, r, g, b) {
              return r + r + g + g + b + b;
          });
          result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
      } else {
          var x = colorsToHex[hex];
          if (x) {
            result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(x);
          }
      }
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : null;

    }
	
	function rgbToHsl(r, g, b){
		r /= 255, g /= 255, b /= 255;
		var max = Math.max(r, g, b), min = Math.min(r, g, b);
		var h, s, l = (max + min) / 2;

		if(max == min){
			h = s = 0; // achromatic
		}else{
			var d = max - min;
			s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
			switch(max){
				case r: h = (g - b) / d + (g < b ? 6 : 0); break;
				case g: h = (b - r) / d + 2; break;
				case b: h = (r - g) / d + 4; break;
			}
			h /= 6;
		}

		return [h, s, l];
	}

    function stringStartsWith (string, prefix) {
      return string.slice(0, prefix.length) == prefix;
    }

}(Highcharts));
